package binding

import (
	"fmt"
	"github.com/go-playground/validator/v10"
	"reflect"
	"strings"
	"sync"
)

type ValidatorStruct interface {
	Validate(obj interface{}) error
}

type defaultValidator struct {
	once      sync.Once
	validator *validator.Validate
}

type SliceValidationError []error

/*
在 Go 语言中，切片（slice）是一个包含三个部分的数据结构：一个指向数组的指针、长度（length）和容量（capacity）。
当我们说“复制切片”时，指的是复制这三个字段，而不是复制切片所指向的整个底层数组的数据。也就是说，原始切片和复制后的切片会共享同一段底层数组。

对于 Error() 方法来说，因为它仅需要读取 SliceValidationError 的状态，并不需要修改其状态，所以使用值类型作为接收者就足够了。
当你使用值类型作为方法的接收者时，Go 会在每次方法调用时复制该值。由于本例中的错误切片可能包含大量元素，使用值类型可能会导致大量内存复制。
然而，需要注意的是，虽然 SliceValidationError 自身被复制了，但是切片内部的数据并没有被复制。
也就是说，这个复制操作只涉及到切片头部的复制，而非整个切片数据的复制，这通常不会引起重大的性能问题。
*/
func (err SliceValidationError) Error() string {
	if len(err) == 0 {
		return ""
	}
	/*
		在 Go 中，var b strings.Builder 这种声明方式会创建一个 strings.Builder 类型的变量 b 并自动将其初始化为该类型的零值。
		对于大多数数据类型，包括所有的 struct 类型（像 strings.Builder），它们的零值都是所谓的空值。对于结构体来说，这意味着它的所有字段都会被初始化为相应类型的零值。
		strings.Builder 是一个结构体，它的零值完全可用。也就是说，你可以立即开始使用这个零值 Builder 调用方法，而不必进行任何其他工作。
		因此，虽然你没有明确地“实例化”一个 strings.Builder，但 var b strings.Builder 这行代码确实已经创建了一个可以立即使用的 Builder
	*/
	var b strings.Builder

	for i, e := range err {
		if e != nil {
			if i != 0 {
				b.WriteString("\n")
			}
			_, _ = fmt.Fprintf(&b, "[%d]: %s", i, e.Error())
		}
	}
	return b.String()
}

func (dv *defaultValidator) Validate(obj interface{}) error {
	if obj == nil {
		return nil
	}

	val := reflect.ValueOf(obj)
	switch val.Kind() {
	case reflect.Ptr:
		return dv.Validate(val.Elem().Interface())
	case reflect.Struct:
		return dv.validate(obj)
	case reflect.Slice, reflect.Array:
		count := val.Len()
		errors := make(SliceValidationError, count)
		for i := 0; i < count; i++ {
			if err := dv.Validate(val.Index(i).Interface()); err != nil {
				errors = append(errors, err)
			}
		}
		return errors
	default:
		return nil
	}
}

func (dv *defaultValidator) validate(obj interface{}) error {
	dv.init()
	return dv.validator.Struct(obj)
}

func (dv *defaultValidator) init() {
	dv.once.Do(func() {
		dv.validator = validator.New()
		dv.validator.SetTagName("binding")
	})
}
